#include "libxluploader.h"
#include <assert.h>
#include <iostream>


//////////////////////////////////////////////////////////////////////////
namespace {
class Initializer {
public:
	Initializer() {
		curl_global_init(CURL_GLOBAL_DEFAULT);
	}
};
Initializer initializer;
const char *USER_AGENT = "Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6";
static std::string s_proxy;

//////////////////////////////////////////////////////////////////////////
// typedef

typedef std::vector<std::string> _vs;

//////////////////////////////////////////////////////////////////////////
// local functions

/**
 * @brief parse cookie from Set-Cookie to cookie (Cookie)
 */
std::string parse_cookie(const std::string &cookie) {
	static char* ignores[] = {
		"expires",
		"domain",
		"path",
	}; 

	std::string cookie_string;
	_vs kvs = util_explode("; ", cookie);
	for (_vs::const_iterator it = kvs.begin(); it != kvs.end(); ++ it) {
		const char *s = it->c_str();
		if (strchr(s, '=') == NULL) {
			continue; // SECURE or HTTPONLY
		}
		_vs kv = util_explode("=", s, 2);
		assert (kv.size() == 2);
		const char *key = kv[0].c_str();
		const char *value = kv[1].c_str();
		bool skip = false;
		for (int i = 0; i < COUNT_OF(ignores); ++ i) {
			if (_stricmp(key, ignores[i]) == 0) {
				skip = true;
				break;
			}
		}
		if (skip) {
			continue; // skip
		}

		if (cookie_string.length() > 0) {
			cookie_string += "; ";
		}
		cookie_string += *it;
	}

	return cookie_string;
}

//////////////////////////////////////////////////////////////////////////

bool parse_http_response(const std::string &http_response, std::string &html, std::string &cookie) {
	std::string response = http_response;
	std::string::size_type index = response.find("\r\n\r\n");
	if (index == std::string::npos) {
		return false; // but this shouldn't happen :)
	}

	html = response.substr(index + 4);
	std::string headers = response.substr(0, index);
	_vs lines = util_explode("\r\n", headers);
	_vs::iterator it = lines.begin();
	++ it; // skip the status line (for example: HTTP/1.1 200 OK)
	for (; it != lines.end(); ++ it) {
		_vs kv = util_explode(": ", *it);
		if (kv.size() != 2) {
			continue;
		}
		if (_stricmp(kv[0].c_str(), "Set-Cookie") == 0) {
			std::string cookie_line = parse_cookie(kv[1]);

			// if already has, ignore it
			if (strstr(cookie.c_str(), cookie_line.c_str()) != NULL) {
				continue;
			}

			if (cookie.length() > 0) {
				cookie += "; ";
			}
			cookie += cookie_line;
		}
	}

	return true;
}

}

//////////////////////////////////////////////////////////////////////////
// defines
#define DEBUG_UPLOAD


//////////////////////////////////////////////////////////////////////////
// STATIC
static const long CONNECT_TIMEOUT = 10L;



//////////////////////////////////////////////////////////////////////////
// prototypes

/**
 * used for curl_easy_perform() to get the fetched data
 */
static int func_get_http_data(char *data, size_t size, size_t nmemb, std::string *writerData);

/**
 * travel the dom tree, to get the form which has at least one input which type is "file"
 * @index the index-th form
 */
static HtmlNodeIter func_get_upload_form (HtmlDom &dom, int index = 0);

/**
 * only for debugging
 */
static void __cdecl debug_upload_dump (const char *format, ...);



//////////////////////////////////////////////////////////////////////////
// HTTP 

#define HTTP_CHECK_RESULT(curl, res)	do {\
                                               if (res != CURLE_OK) {\
                                                   if (curl != NULL) {\
                                                       curl_easy_cleanup(curl);\
						   }\
                                                   return res;\
                                               }\
                                        }while (0)

void http_set_proxy(const std::string &proxy) {
	s_proxy = proxy;
}

void http_clear_proxy() {
	s_proxy.clear();
}

HttpCode http_get (
                   std::string &output,
                   const char *url,
                   std::string &cookie, 
                   HttpProgressCallback callback,
                   void *callback_param,
                   std::string referer
                  ) {
	CURL *curl = NULL;
	CURLcode res = CURLE_FAILED_INIT;

	curl = curl_easy_init();
	if (curl) {
		res = curl_easy_setopt(curl, CURLOPT_URL, url);
		HTTP_CHECK_RESULT(curl, res);
		res = curl_easy_setopt(curl, CURLOPT_HEADER, 1L);
		HTTP_CHECK_RESULT(curl, res);
		res = curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, CONNECT_TIMEOUT);
		HTTP_CHECK_RESULT(curl, res);
		res = curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
		HTTP_CHECK_RESULT(curl, res);
		res = curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L);
		HTTP_CHECK_RESULT(curl, res);
		res = curl_easy_setopt(curl, CURLOPT_USERAGENT, USER_AGENT);
		HTTP_CHECK_RESULT(curl, res);

		if (cookie.length() > 0) {
			res = curl_easy_setopt(curl, CURLOPT_COOKIE, cookie.c_str());
			HTTP_CHECK_RESULT(curl, res);
		}

		if (referer.length() > 0) {
			res = curl_easy_setopt(curl, CURLOPT_REFERER, referer.c_str());
			HTTP_CHECK_RESULT(curl, res);
		}

		if (s_proxy.length() > 0) {
			res = curl_easy_setopt(curl, CURLOPT_PROXY, s_proxy.c_str());
			HTTP_CHECK_RESULT(curl, res);
		}

		if (callback != NULL) {
			res = curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, callback);
			HTTP_CHECK_RESULT(curl, res);
			res = curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, callback_param);
			HTTP_CHECK_RESULT(curl, res);
			res = curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
			HTTP_CHECK_RESULT(curl, res);
		}
		

		res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, func_get_http_data);
		HTTP_CHECK_RESULT(curl, res);
		res = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &output);
		HTTP_CHECK_RESULT(curl, res);

		res = curl_easy_perform(curl);
		HTTP_CHECK_RESULT(curl, res);

		curl_easy_cleanup(curl);

		if (res == CURLE_OK) {
			bool parsed = parse_http_response(output, output, cookie);
			assert (parsed);
		}
	}

	return res;
}

HttpCode http_post (
		    std::string &output,
                    const std::string &url,
                    HtmlNodeIterArray inputs,
                    const TCHAR *file_pathname,
                    std::string &cookie,
                    HttpProgressCallback callback,
                    void *callback_param,
                    const std::string referer
                   ) {
	CURL *curl = NULL;
	CURLcode res = CURLE_FAILED_INIT;
	std::string file_contents;
	if (!file_get_contents(file_pathname, file_contents)) {
		std::cout << "read " << file_pathname << " failed" << std::endl;
		return CURLE_FILE_COULDNT_READ_FILE;
	}

	// char file_name_utf8[MAX_PATH];
	std::string file_name_utf8;
#ifdef UNICODE
	file_name_utf8 = util_w2utf8(file_pathname);
	// file_put_contents(_T("utf8.txt"), file_name_utf8);
#else
	file_name_utf8 = file_pathname;
#endif
	std::string empty_file_name;
	std::string empty_file_content;

	struct curl_httppost *formpost = NULL;
	struct curl_httppost *lastptr = NULL;
	struct curl_slist *headerlist = NULL;

	assert (inputs.size() > 0);
	res = CURLE_OK;
	CURLFORMcode fc;
	bool file_attached = false;
	for (HtmlNodeIterArray::iterator it = inputs.begin(); it != inputs.end(); ++ it) {
		HtmlNodeIter itNode = *it;
		itNode->parseAttributes();
		std::string type = itNode->attribute("type").second;
		if (type == "radio") {
			if (itNode->attribute("checked").second == "checked") {
				fc = curl_formadd(&formpost, &lastptr, 
					CURLFORM_COPYNAME, itNode->attribute("name").second.c_str(),
					CURLFORM_COPYCONTENTS, itNode->attribute("value").second.c_str(),
					CURLFORM_END);
				if (fc != CURL_FORMADD_OK) {
					curl_formfree(formpost);
					return CURLE_OUT_OF_MEMORY;
				}
			}
		} else if (type == "file") {
			if (!file_attached) {
				std::string mime = util_get_mime_by_name(file_pathname);
				if (mime.length() == 0) {
					fc = curl_formadd(&formpost, &lastptr, 
						CURLFORM_COPYNAME, itNode->attribute("name").second.c_str(),
						CURLFORM_BUFFER, file_name_utf8.c_str(),
						CURLFORM_BUFFERPTR, file_contents.c_str(),
						CURLFORM_BUFFERLENGTH, file_contents.length(),
						CURLFORM_END);
				} else {
					fc = curl_formadd(&formpost, &lastptr, 
						CURLFORM_COPYNAME, itNode->attribute("name").second.c_str(),
						CURLFORM_BUFFER, file_name_utf8.c_str(),
						CURLFORM_BUFFERPTR, file_contents.c_str(),
						CURLFORM_BUFFERLENGTH, file_contents.length(),
						CURLFORM_CONTENTTYPE, mime.c_str(),
						CURLFORM_END);
				}
				file_attached = true;
			} else {
				fc = curl_formadd(&formpost, &lastptr, 
					CURLFORM_COPYNAME, itNode->attribute("name").second.c_str(),
					CURLFORM_BUFFER, empty_file_name.c_str(),
					CURLFORM_BUFFERPTR, empty_file_content.c_str(),
					CURLFORM_BUFFERLENGTH, empty_file_content.length(),
					CURLFORM_END);
			}

			if (fc != CURL_FORMADD_OK) {
				curl_formfree(formpost);
				return CURLE_OUT_OF_MEMORY;
			}
		} else {
			fc = curl_formadd(&formpost, &lastptr, 
				CURLFORM_COPYNAME, itNode->attribute("name").second.c_str(),
				CURLFORM_COPYCONTENTS, itNode->attribute("value").second.c_str(),
				CURLFORM_END);
			if (fc != CURL_FORMADD_OK) {
				curl_formfree(formpost);
				return CURLE_OUT_OF_MEMORY;
			}
		}
	}

	res = CURLE_FAILED_INIT;
	curl = curl_easy_init();
	if (curl) {
		res = curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
		HTTP_CHECK_RESULT(curl, res);
		res = curl_easy_setopt(curl, CURLOPT_HEADER, 1L);
		HTTP_CHECK_RESULT(curl, res);
		res = curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, CONNECT_TIMEOUT);
		HTTP_CHECK_RESULT(curl, res);
		res = curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
		HTTP_CHECK_RESULT(curl, res);
		res = curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L);
		HTTP_CHECK_RESULT(curl, res);

		res = curl_easy_setopt(curl, CURLOPT_USERAGENT, USER_AGENT);
		HTTP_CHECK_RESULT(curl, res);

		if (cookie.length() > 0) {
			res = curl_easy_setopt(curl, CURLOPT_COOKIE, cookie.c_str());
			HTTP_CHECK_RESULT(curl, res);
		}

		if (referer.length() > 0) {
			res = curl_easy_setopt(curl, CURLOPT_REFERER, referer.c_str());
			HTTP_CHECK_RESULT(curl, res);
		}

		if (s_proxy.length() > 0) {
			res = curl_easy_setopt(curl, CURLOPT_PROXY, s_proxy.c_str());
			HTTP_CHECK_RESULT(curl, res);
		}

		res = curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
		HTTP_CHECK_RESULT(curl, res);

		if (callback != NULL) {
			res = curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, callback);
			HTTP_CHECK_RESULT(curl, res);
			res = curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, callback_param);
			HTTP_CHECK_RESULT(curl, res);
			res = curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
			HTTP_CHECK_RESULT(curl, res);
		}

		res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, func_get_http_data);
		HTTP_CHECK_RESULT(curl, res);
		res = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &output);
		HTTP_CHECK_RESULT(curl, res);

		res = curl_easy_perform(curl);
		HTTP_CHECK_RESULT(curl, res);

		curl_easy_cleanup(curl);
		curl_formfree(formpost);

		if (res == CURLE_OK) {
			bool parsed = parse_http_response(output, output, cookie);
			assert (parsed);
		}

	}

	return res;
}

/**
 * @note return CURLE_REMOTE_FILE_NOT_FOUND means can't find the form.
 */
HttpCode http_upload (
		      std::string &output,
                      const std::string &url,
                      const TCHAR *file_pathname,
                      std::string &cookie,
                      HttpProgressCallback callback,
                      void *callback_param
                     ) {
	// 1. get html
	std::string html;
	CURLcode res = http_get(html, url.c_str(), cookie, callback, callback_param);
	if (res != CURLE_OK) {
		return res;
	}

	// 2. get dom
	HtmlDom dom = html_load(html);

	// 3. get the form & inputs
	HtmlNodeIterArray::iterator it;
	HtmlNodeIterArray forms = html_get_nodes_by_tag(dom, "form");
	HtmlNodeIterArray inputs;
	HtmlNodeIter itForm;
	for (it = forms.begin(); it != forms.end(); ++ it) {
		inputs = html_get_children(dom, *it, "input");
		HtmlNodeIterArray::iterator itInput;
		for (itInput = inputs.begin(); itInput != inputs.end(); ++ itInput) {
			(*itInput)->parseAttributes();
			if (_stricmp((*itInput)->attribute("type").second.c_str(), "file") == 0) {
				break;
			}
		}
		if (itInput != inputs.end()) {
			// found the form
			itForm = *it;
			break;
		}
	}
	if (it == forms.end()) {
		return CURLE_REMOTE_FILE_NOT_FOUND; // no form found
	}

	// 4. get post url
	itForm->parseAttributes();
	std::string post_url = itForm->attribute("action").second;
	post_url = util_get_form_action_url(url, post_url);
	debug_upload_dump("post to: %s\n", post_url.c_str());

	// post
	std::string referer = url;
	return http_post(output, post_url, inputs, file_pathname, cookie, callback, callback_param, referer);
}


//////////////////////////////////////////////////////////////////////////
// static 

/**
 * @brief a callback function to get data from curl.
 */
int func_get_http_data(char *data, size_t size, size_t nmemb, std::string *writerData)
{
	if (writerData == NULL)
		return 0;

	writerData->append(data, size * nmemb);

	return size * nmemb;
}

/**
 * @brief get the form node iterator by checking whether it has input which type is 'file'
 */
HtmlNodeIter func_get_upload_form (HtmlDom &dom, int index) {
	int pos = -1;
	HtmlNodeIter it, end = dom.end();
	for (it = dom.begin(); it != end; ++ it) {
		
	}

	return it;
}


//////////////////////////////////////////////////////////////////////////
// utilities
#define DEBUG_UPLOAD
#ifdef DEBUG_UPLOAD
void __cdecl debug_upload_dump (const char *format, ...) {
	va_list arg_list;
	va_start (arg_list, format);
	vprintf (format, arg_list);
	va_end (arg_list);
}
#else
void __cdecl debug_upload_dump (const char *format, ...) {}
printf
#endif
